package com.yuntian.baidu;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.SortedMap;
import java.util.StringJoiner;
import java.util.TreeMap;
import org.apache.commons.codec.binary.Hex;
import javax.net.ssl.*;
import java.security.cert.X509Certificate;

/**
 * YthBdpApiExecuteClient
 */
public class YthBdpApiExecuteClient {
    //定义签名算法产量区域
    //签名算法
    private static final String BCE_AUTH_VERSION = "bce-auth-v1";
    // BCE 认证版本
    private static final String BCE_PREFIX_LOWER_CASE = "x-bce-";
    // 签名头
    private static final String SIGNATURE_HEADER_NAME = "X-Bce-Signature";
    // 默认签名头
    private static final Set<String> DEFAULT_HEADERS_TO_SIGN = new HashSet<>(Arrays.asList(
            "host", "content-length", "content-type", "content-md5"));
    // 时间格式
    private static final DateTimeFormatter DATE_TIME_FORMATTER =
            DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
                    .withZone(ZoneOffset.UTC);
    // 换行符
    private static final String HEADER_JOINER = "\n";
    // 参数分隔符
    private static final String QUERY_STRING_JOINER = "&";
    // 签名头分隔符
    private static final String SIGNED_HEADER_STRING_JOINER = ";";
    //密钥信息
    private final Credential credential;
    // 构造函数
    public YthBdpApiExecuteClient(Credential credential) {
        this.credential = credential;
    }
    // 初始化SSLContext，用于信任所有证书（生产环境需谨慎使用）
    static {
        try {
            SSLContext sslContext = SSLContext.getInstance("SSL");
            sslContext.init(null, new TrustManager[]{new X509TrustManager() {
                public X509Certificate[] getAcceptedIssuers() { return null; }
                public void checkClientTrusted(X509Certificate[] certs, String authType) {}
                public void checkServerTrusted(X509Certificate[] certs, String authType) {}
            }}, new java.security.SecureRandom());
            HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
            HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true);
        } catch (Exception e) {
            throw new RuntimeException("SSL初始化失败", e);
        }
    }
    /**
     * 通过参数签名请求
     * <p>
     * 签名标头将添加到传入的请求中。
     *
     * @param method  请求方法
     * @param uri     路径和查询将用于生成签名
     * @param headers 必须包含host进请求头
     * @param options nullable
     */
    public void sign(String method, URI uri, Map<String, String> headers, SignOptions options) {
        // 定义签名算法产量区域
        Map<String, String> httpHeaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
        httpHeaders.putAll(headers);
        //  必须包含host进请求头
        Objects.requireNonNull(httpHeaders.get("host"), "Host header is required.");

        if (options == null) {
            options = SignOptions.DEFAULT;
        }

        String timestamp = options.getTimestamp();

        if (timestamp == null) {
            timestamp = DATE_TIME_FORMATTER.format(Instant.now());
        }

        String authString = BCE_AUTH_VERSION + "/" + credential.getAccessKey() + "/"
                + timestamp + "/" + options.getExpirationInSeconds();
        // 签名算法
        String signingKey = SignUtils.sha256Hex(credential.getSecretKey(), authString);
        // Formatting the URL with signing protocol.
        String canonicalURI = this.getCanonicalURIPath(uri.getPath());
        // Formatting the query string with signing protocol.
        String canonicalQueryString = getCanonicalQueryString(uri.getRawQuery());
        // Sorted the headers should be signed from the request.
        SortedMap<String, String> headersToSign =
                getHeadersToSign(httpHeaders, options.getHeadersToSign());
        // Formatting the headers from the request based on signing protocol.
        String canonicalHeader = this.getCanonicalHeaders(headersToSign);
        String signedHeaders = "";
        if (options.getHeadersToSign() != null) {
            signedHeaders = SignUtils.join(SIGNED_HEADER_STRING_JOINER, headersToSign.keySet());
            signedHeaders = signedHeaders.trim().toLowerCase();
        }

        String canonicalRequest = method.toUpperCase() + "\n" + canonicalURI + "\n"
                + canonicalQueryString + "\n" + canonicalHeader;

        // Signing the canonical request using key with sha-256 algorithm.
        String signature = SignUtils.sha256Hex(signingKey, canonicalRequest);

        String signatureHeaderValue = authString + "/" + signedHeaders + "/" + signature;

        System.out.println("CanonicalRequest:" + canonicalRequest.replace("\n", "[\\n]"));
        System.out.println(SIGNATURE_HEADER_NAME + ":" + signatureHeaderValue);

        headers.put(SIGNATURE_HEADER_NAME, signatureHeaderValue);
    }

    private String getCanonicalURIPath(String path) {
        if (path == null) {
            return "/";
        } else if (path.startsWith("/")) {
            return SignUtils.normalizePath(path);
        } else {
            return "/" + SignUtils.normalizePath(path);
        }
    }

    private static String getCanonicalQueryString(String queryStr) {
        if (queryStr == null || queryStr.isEmpty()) {
            return "";
        }

        List<String> queryStrings = new ArrayList<>();

        for (String pair : queryStr.split("&")) {
            String[] kv = pair.split("=", 2);
            String queryString = SignUtils.normalize(kv[0]) + "=";
            if (kv.length == 2) {
                queryString += SignUtils.normalize(kv[1]);
            }
            queryStrings.add(queryString);
        }

        Collections.sort(queryStrings);

        return SignUtils.join(QUERY_STRING_JOINER, queryStrings);
    }

    // Get the canonical headers and signed headers.
    private String getCanonicalHeaders(SortedMap<String, String> headers) {
        if (headers.isEmpty()) {
            return "";
        }

        List<String> headerStrings = new ArrayList<>();
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            String key = entry.getKey();
            if (key == null) {
                continue;
            }
            String value = entry.getValue();
            if (value == null || value.isEmpty()) {
                throw new RuntimeException("Header to sign should have non-empty value.");
            }
            headerStrings.add(SignUtils.normalize(key.trim().toLowerCase()) + ':' + SignUtils.normalize(value.trim()));
        }
        Collections.sort(headerStrings);

        return SignUtils.join(HEADER_JOINER, headerStrings);
    }

    // Get the headers to sign.
    private SortedMap<String, String> getHeadersToSign(Map<String, String> headers, Set<String> headersToSign) {
        SortedMap<String, String> ret = new TreeMap<>();
        if (headersToSign != null) {
            Set<String> tempSet = new HashSet<>();
            for (String header : headersToSign) {
                tempSet.add(header.trim().toLowerCase());
            }
            headersToSign = tempSet;
        }
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            String key = entry.getKey();
            if (entry.getValue() != null && !entry.getValue().isEmpty()) {
                boolean isDefaultHeaderToSign = headersToSign == null && this.isDefaultHeaderToSign(key);
                boolean isNeededHeaderToSign = (headersToSign != null && headersToSign.contains(key.toLowerCase())
                        && !SIGNATURE_HEADER_NAME.equalsIgnoreCase(key));
                if (isDefaultHeaderToSign || isNeededHeaderToSign) {
                    ret.put(key, entry.getValue());
                }
            }
        }
        return ret;
    }

    //  判断是否为默认的Header
    private boolean isDefaultHeaderToSign(String header) {
        header = header.trim().toLowerCase();
        return header.startsWith(BCE_PREFIX_LOWER_CASE) || DEFAULT_HEADERS_TO_SIGN.contains(header);
    }


    // 定义Credential类，用于存储AccessKey和SecretKey
    public static class Credential {
        private String accessKey;
        private String secretKey;

        public Credential(String accessKey, String secretKey) {
            this.accessKey = accessKey;
            this.secretKey = secretKey;
        }

        public String getAccessKey() {
            return accessKey;
        }

        public void setAccessKey(String accessKey) {
            this.accessKey = accessKey;
        }

        public String getSecretKey() {
            return secretKey;
        }

        public void setSecretKey(String secretKey) {
            this.secretKey = secretKey;
        }
    }

    // 定义SignOptions类，用于存储签名选项
    public static class SignOptions {

        public static final SignOptions DEFAULT = new SignOptions();

        private String timestamp;
        private int expirationInSeconds = 1800;
        private Set<String> headersToSign;

        public String getTimestamp() {
            return timestamp;
        }

        public void setTimestamp(String timestamp) {
            this.timestamp = timestamp;
        }

        public int getExpirationInSeconds() {
            return expirationInSeconds;
        }

        public void setExpirationInSeconds(int expirationInSeconds) {
            this.expirationInSeconds = expirationInSeconds;
        }

        public Set<String> getHeadersToSign() {
            return headersToSign;
        }

        public void setHeadersToSign(Set<String> headersToSign) {
            this.headersToSign = headersToSign;
        }
    }

    //  定义签名工具类类，用于生成签名
    public static class SignUtils {

        private static final BitSet URI_UNRESERVED_CHARACTERS = new BitSet();
        private static final String[] PERCENT_ENCODED_STRINGS = new String[256];
        private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray();

        static {
            for (int i = 'a'; i <= 'z'; i++) {
                URI_UNRESERVED_CHARACTERS.set(i);
            }
            for (int i = 'A'; i <= 'Z'; i++) {
                URI_UNRESERVED_CHARACTERS.set(i);
            }
            for (int i = '0'; i <= '9'; i++) {
                URI_UNRESERVED_CHARACTERS.set(i);
            }
            URI_UNRESERVED_CHARACTERS.set('-');
            URI_UNRESERVED_CHARACTERS.set('.');
            URI_UNRESERVED_CHARACTERS.set('_');
            URI_UNRESERVED_CHARACTERS.set('~');

            for (int i = 0; i < PERCENT_ENCODED_STRINGS.length; ++i) {
                PERCENT_ENCODED_STRINGS[i] = String.format("%%%02X", i);
            }
        }

        public static String join(String joinerStr, Iterable<String> iterable) {
            StringJoiner joiner = new StringJoiner(joinerStr);
            iterable.forEach(joiner::add);
            return joiner.toString();
        }

        public static String normalizePath(String path) {
            return normalize(path).replace("%2F", "/");
        }

        /**
         * Normalize a string for use in BCE web service APIs. The normalization algorithm is:
         * <p>
         * <ol>
         *   <li>Convert the string into a UTF-8 byte array.</li>
         *   <li>Encode all octets into percent-encoding, except all URI unreserved characters per the RFC 3986.</li>
         * </ol>
         *
         * <p>
         * All letters used in the percent-encoding are in uppercase.
         *
         * @param value the string to normalize.
         * @return the normalized string.
         */
        public static String normalize(String value) {
            StringBuilder builder = new StringBuilder();
            for (byte b : value.getBytes(StandardCharsets.UTF_8)) {
                if (URI_UNRESERVED_CHARACTERS.get(b & 0xFF)) {
                    builder.append((char) b);
                } else {
                    builder.append(PERCENT_ENCODED_STRINGS[b & 0xFF]);
                }
            }
            return builder.toString();
        }

        private static String sha256Hex(String signingKey, String stringToSign) {
            try {
                Mac instance = Mac.getInstance("HmacSHA256");
                instance.init(new SecretKeySpec(signingKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
                return bytesToHex(instance.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8)));
            } catch (Exception e) {
                throw new RuntimeException("Failed to generate signature.", e);
            }
        }

        private static String bytesToHex(byte[] bytes) {
            return Hex.encodeHexString(bytes);
        }
    }

    /**
     * 调用示例，仅作为示例，在正式使用时，需根据具体情况进行封装
     */
    public static void main(String[] args) throws IOException {
        // 应用 appKey
        String accessKey = "65e9bffe164f4b9b831da45524b664ad";
        // 应用 appSecret
        String secretKey = "ed4b0a37520f453e920bb98501f3eaf4";

       /* // GET请求地址
        String url = "https://api.ythit.cn/md/api/getSapPersonDetails";
        // GET请求参数
        String param = "pageSize=100&pageNum=1";
        // GET方式请求
        getRequestByCondition(url,param,accessKey, secretKey);*/

        // POST请求地址
        String url = "http://gwgp-wcjmjlpkro4.i.apiverse.ythit.cn/ythAI/basicData/orgUnit/v1";
        // POST请求参数
        String param = "pageSize=100&pageNum=1&userCode=YTH_AI&authCode=wIxyNE9O0btbGiRAWnjM2Q==";
        // POST请求Content-Type
        String contentType = "application/x-www-form-urlencoded;charset=utf-8";
        // POST方式请求
        String s = postRequestByCondition(url,contentType,param,accessKey, secretKey);
    }


    private static void getRequestByCondition(String url,String param,String accessKey, String secretKey) throws IOException {
        String httpMethod = "GET";
        // accessKey, secretKey 参与认证签名
        Credential credential = new Credential(accessKey, secretKey);
        YthBdpApiExecuteClient signer = new YthBdpApiExecuteClient(credential);
        url = url + "?" + param;
        URI uri = URI.create(url);
        Map<String, String> headers = new HashMap<>();
        headers.put("Host", uri.getHost());

        // Signature header will be added to headers.
        SignOptions options = SignOptions.DEFAULT;
        HashSet<String> headersToSign = new HashSet<>();
        headersToSign.add("Host");
        options.setHeadersToSign(headersToSign);
        signer.sign(httpMethod, uri, headers, options);

        // 发送请求
        String s = getRequest(url, httpMethod, headers);
        System.out.println(s);
    }

    // 发送GET请求并获取响应
    private static String getRequest(String url, String httpMethod, Map<String, String> headers) throws IOException {
        URL uri = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) uri.openConnection();
        connection.setRequestMethod(httpMethod);

        for (Map.Entry<String, String> entry : headers.entrySet()) {
            connection.setRequestProperty(entry.getKey(), entry.getValue());
        }

        connection.setDoOutput(true);

        int responseCode = connection.getResponseCode();
        if (responseCode == HttpURLConnection.HTTP_OK) {
            BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
            String inputLine;
            StringBuilder response = new StringBuilder();
            while ((inputLine = in.readLine()) != null) {
                response.append(inputLine);
            }
            in.close();
            return response.toString();
        } else {
            System.out.println("GET request failed, status: " + responseCode);
            return "";
        }
    }

    public static String postRequestByCondition(String url,String contentType,String param,String accessKey, String secretKey) throws IOException {
        // 请求方法设置为POST
        String httpMethod = "POST";
        // accessKey, secretKey 参与认证签名
        Credential credential = new Credential(accessKey, secretKey);
        // 创建实例
        YthBdpApiExecuteClient signer = new YthBdpApiExecuteClient(credential);
        /// 设置请求参数URI对象
        URI uri = URI.create(url);
        //  设置请求头
        Map<String, String> headers = new HashMap<>();
        // 请求头里设置Host
        headers.put("Host", uri.getHost());

        // Signature header will be added to headers.
        SignOptions options = SignOptions.DEFAULT;
        HashSet<String> headersToSign = new HashSet<>();
        headersToSign.add("Host");
        options.setHeadersToSign(headersToSign);
        signer.sign(httpMethod, uri, headers, options);

        // 发送请求
        String s = postRequest(url,contentType,param,httpMethod, headers);
        return s;
    }

    private static String postRequest(String url,String contentType,String data,String httpMethod, Map<String, String> headers) throws IOException {
        // 请求方法及请求地址
        URL uri = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) uri.openConnection();
        connection.setRequestMethod(httpMethod);
        connection.setConnectTimeout(6000);
        connection.setReadTimeout(6000);
        connection.setInstanceFollowRedirects(true);
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            connection.setRequestProperty(entry.getKey(), entry.getValue());
        }

        // 处理POST请求体
        if ("POST".equalsIgnoreCase(httpMethod)) {
            connection.setDoOutput(true);
            connection.setRequestProperty("Content-Type", contentType);
        }
        connection.setDoOutput(true);
        OutputStream outputStream = connection.getOutputStream();
        outputStream.write(data.getBytes());
        outputStream.flush();
        outputStream.close();

        int responseCode = connection.getResponseCode();
        if (responseCode == HttpURLConnection.HTTP_OK) {
            BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
            String inputLine;
            StringBuilder response = new StringBuilder();
            while ((inputLine = in.readLine()) != null) {
                response.append(inputLine);
            }
            in.close();
            return response.toString();
        } else {
            System.out.println("POST request failed, status: " + responseCode);
            return "";
        }
    }
}
